import copy
import arrow

from bson import SON
from pymongo import (
    UpdateOne as PymongoUpdateOne,
    InsertOne as PymongoInsertOne,
    DeleteOne as PymongoDeleteOne,
)
from django_mongoengine import Document, EmbeddedDocument
from django_mongoengine.document import QuerySetManager
from mongoengine.queryset import QuerySet
from mongoengine.errors import ValidationError
from django_mongoengine.paginator import Paginator
from mongoengine import fields
from mongoengine.queryset.queryset import QuerySetNoCache as SysQuerySetNoCache
from utilities.utils import make_id
from utilities.errors import CustomAPIException


class UpdateOne(PymongoUpdateOne):
    pass


class InsertOne(PymongoInsertOne):
    pass


class DeleteOne(PymongoDeleteOne):
    pass


class QuerySetMixin:

    def filter(self, *q_objs, **query):
        """An alias of :meth:`~mongoengine.queryset.QuerySet.__call__`"""
        copy_query = {}
        for k, v in query.items():
            if isinstance(v, dict) and v.get("$in"):
                copy_query.update({f"{k}__in": v.get("$in")})
            else:
                copy_query.update({k: v})
        return self.__call__(*q_objs, **copy_query)


class CustomQuerySet(QuerySetMixin, QuerySet):

    def find(self, _filter: dict = None):
        if not _filter:
            return self().all()
        else:
            return self(__raw__=_filter)

    def find_one(self, _filter: dict = None):
        if not _filter:
            _filter = {}
        results = self.find(_filter).limit(1)
        if results:
            return results[0]

        return None

    def paginate(self, per_page, **kwargs):
        """
        Paginate the QuerySet with a certain number of docs per page
        and return docs for a given page.
        """
        return Paginator(self, per_page)

    def bulk_write(self, requests, **kwargs):
        if requests:
            self._collection.bulk_write(requests=requests, **kwargs)


class QuerySetNoCache(QuerySetMixin, SysQuerySetNoCache):

    def __len__(self):
        _count = 0
        tmp_obj = copy.copy(self)
        for item in tmp_obj:
            _count += 1
        return _count


class BaseDocument(Document):
    created_at = fields.DateTimeField()
    updated_at = fields.DateTimeField()

    meta = {
        'strict': False,
        'abstract': True,
        'queryset_class': CustomQuerySet,
    }

    def validate(self, clean=True):
        try:
            super(BaseDocument, self).validate()
        except ValidationError as e:
            raise CustomAPIException(e=e)

    def to_dict(self):
        return self.to_mongo().to_dict()

    @classmethod
    def from_dict(cls, dict_data, created=False):
        return cls._from_son(SON(dict_data), created=created)

    def save(self, *args, **kwargs):
        now_datetime = arrow.utcnow().datetime
        pk = self._meta.get('primary_key', '')
        if pk and not getattr(self, pk):
            setattr(self, pk, make_id())
        if not self.created_at:
            self.created_at = now_datetime
        self.updated_at = now_datetime
        return super(BaseDocument, self).save(*args, **kwargs)

    def update(self, **kwargs):
        kwargs['updated_at'] = arrow.utcnow().datetime
        return super(BaseDocument, self).update(**kwargs)


class BaseEmbeddedDocument(EmbeddedDocument):
    meta = {'abstract': True, 'strict': False}

    def validate(self, clean=True):
        try:
            super(BaseEmbeddedDocument, self).validate()
        except ValidationError as e:
            raise CustomAPIException(e=e)

    def to_dict(self):
        return self.to_mongo().to_dict()

    @classmethod
    def from_dict(cls, dict_data, created=False):
        """Converts dict data to a Document instance

        :param str dict_data: The dict data to load into the Document
        :param bool created: Boolean defining whether to consider the newly
            instantiated document as brand new or as persisted already:
            * If True, consider the document as brand new, no matter what data
              it's loaded with (i.e. even if an ID is loaded).
            * If False and an ID is NOT provided, consider the document as
              brand new.
            * If False and an ID is provided, assume that the object has
              already been persisted (this has an impact on the subsequent
              call to .save()).
            * Defaults to ``False``.
        """
        return cls._from_son(SON(dict_data), created=created)

    def items(self):
        return self.to_dict().items()


class IdField(fields.StringField):
    def __init__(self, regex=None, **kwargs):
        super(IdField, self).__init__(
            regex=regex, max_length=32, min_length=32, **kwargs
        )
